package org.de.maeyer.sampler;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

import org.de.maeyer.statistic.Accumulator;
import org.de.maeyer.statistic.CountAccumulator;
import org.de.maeyer.statistic.ElapsedTimeAccumulator;
import org.de.maeyer.statistic.ExtremaAccumulator;
import org.de.maeyer.statistic.LastValueAccumulator;
import org.de.maeyer.statistic.Statistics;
import org.de.maeyer.statistic.StatisticsBuilder;
import org.de.maeyer.statistic.VarianceAccumulator;
import org.de.maeyer.unit.Unit;
import org.de.maeyer.util.Clock;
import org.de.maeyer.util.CpuClock;

/**
 * @author Peter De Maeyer
 */
public class ConcurrentSampler implements Sampler, Counter, ObjectSampler, DoubleSampler, LongSampler {
	
	private static final Clock CLOCK = CpuClock.getInstance();
	
	private final Class valueClass;
	
	private final String name;
	
	private final Unit unit;
	
	private final Collection<Accumulator> accumulators = new LinkedList<Accumulator>();
	
	public ConcurrentSampler(Class valueClass, String name, Unit unit) {
		this.valueClass = valueClass;
		this.name = name;
		this.unit = unit;
	}
	
	@Override
	public synchronized ConcurrentSampler decumulateAll() {
		accumulators.clear();
		return this;
	}
	
	@Override
	public synchronized String getName() {
		return name;
	}
	
	@Override
	public synchronized Unit getUnit() {
		return unit;
	}
	
	@Override
	public synchronized void reset() {
		resetAccumulators();
	}

	@Override
    public synchronized ConcurrentSampler accumulateCount() {
		if (!hasAccumulator(CountAccumulator.class) && !hasAccumulator(VarianceAccumulator.class)) {
			accumulators.add(new CountAccumulator());
		}
		return this;
    }
	
	@Override
	public synchronized ConcurrentSampler accumulateElapsedTime() {
		if (!hasAccumulator(ElapsedTimeAccumulator.class)) {
			accumulators.add(new ElapsedTimeAccumulator(CLOCK));
		}
		return this;
	}

	@Override
    public ConcurrentSampler accumulateRate() {
		return accumulateCount().accumulateElapsedTime();
    }
	
	@Override
	public synchronized ConcurrentSampler accumulateLastValue() {
	    if (!hasAccumulator(LastValueAccumulator.class)) {
	    	accumulators.add(new LastValueAccumulator());
	    }
	    return this;
	}
	
	@Override
	public synchronized ConcurrentSampler accumulateExtrema() {
	    if (!hasAccumulator(ExtremaAccumulator.class)) {
	    	accumulators.add(new ExtremaAccumulator(NaturalComparator.getInstance()));
	    }
	    return this;
	}
	
	@Override
	public synchronized ConcurrentSampler accumulateVariance() {
	    if (!hasAccumulator(VarianceAccumulator.class)) {
	    	removeAccumulator(CountAccumulator.class);
	    	accumulators.add(new VarianceAccumulator());
	    }
	    return this;
	}

	@Override
    public ConcurrentSampler accumulateHistogram(double inclusiveLowerBound, double exclusiveUpperBound, int categoryCount) {
		double[] categoryBounds = new double[categoryCount + 1];
		categoryBounds[0] = inclusiveLowerBound;
		for (int i = 1; i < categoryCount; i++) {
			categoryBounds[i] = inclusiveLowerBound + (exclusiveUpperBound - inclusiveLowerBound) * i / categoryCount;
		}
		categoryBounds[categoryCount] = exclusiveUpperBound;
		return accumulateHistogram(categoryBounds);
    }

	@Override
    public synchronized ConcurrentSampler accumulateHistogram(double... categoryBounds) {
		if (!hasAccumulator(DoubleHistogramAccumulator.class)) {
			accumulators.add(new DoubleHistogramAccumulator(categoryBounds));
		}
		return this;
    }
	
	@Override
	public synchronized void increment() {
		for (Accumulator accumulator: accumulators) {
			accumulator.accumulate();
		}
	}
	
	@Override
	public synchronized void sample(Object value) {
		for (Accumulator accumulator: accumulators) {
			accumulator.accumulateObject(value);
		}
	}

	@Override
    public synchronized void sample(double value) {
		for (Accumulator accumulator: accumulators) {
			accumulator.accumulateDouble(value);
		}
    }

	@Override
    public synchronized void sample(long value) {
		for (Accumulator accumulator: accumulators) {
			accumulator.accumulateLong(value);
		}
    }
	
	@Override
	public synchronized Statistics getStatistics() {
		return buildStatistics();
	}
	
	@Override
	public synchronized Statistics getAndResetStatistics() {
		Statistics statistics = buildStatistics();
		resetAccumulators();
		return statistics;
	}
	
	@Override
	public ConcurrentSampler accumulateHistogram() {
		if (valueClass == Object.class) {
			return accumulateHistogram(-1);
		}
		throw new UnsupportedOperationException();
	}

	@Override
    public synchronized ConcurrentSampler accumulateHistogram(Object... categoryValues) {
		if (!hasAccumulator(ObjectHistogramAccumulator.class)) {
			accumulators.add(new ObjectHistogramAccumulator(categoryValues));
		}
	    return this;
    }

	@Override
    public synchronized ConcurrentSampler accumulateHistogram(int categoryBound) {
		if (!hasAccumulator(ObjectHistogramAccumulator.class)) {
			accumulators.add(new AutoCategorizingObjectHistogramAccumulator(categoryBound));
		}
	    return this;
    }
	
	@Override
	public String toString() {
		if (unit == null) {
			return name;
		} else {
			return name + " (" + unit + ")";
		}
	}
	
	private boolean hasAccumulator(Class clazz) {
		for (Accumulator accumulator: accumulators) {
			if (accumulator.getClass().equals(clazz)) {
				return true;
			}
		}
		return false;
	}
	
	private void removeAccumulator(Class clazz) {
		for (Iterator<Accumulator> it = accumulators.iterator(); it.hasNext(); ) {
			if (it.next().getClass().equals(clazz)) {
				it.remove();
				break;
			}
		}
	}
	
	private Statistics buildStatistics() {
		StatisticsBuilder builder = new StatisticsBuilder(name, unit, valueClass);
		for (Accumulator accumulator: accumulators) {
			accumulator.appendTo(builder);
		}
		return builder.toStatistics();
	}
	
	private void resetAccumulators() {
		for (Accumulator accumulator: accumulators) {
			accumulator.reset();
		}
	}
}
